package src.model.gameWorld;

import geometry.Vector2D;

import java.util.ArrayList;
import java.util.Vector;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import src.model.gameObject.GameObject;
import src.model.gameObject.GameObjectType;
import src.model.gameWorld.pathfinding.PathfindingMap;
import src.model.gameWorld.pathfinding.movement.Direction2D;


/**
 * <b>Overview<b><br>
 * <p>The acutal map is responsible managing the relationship between
 * GameObjects and Positions.  It has the ability to find a Position given
 * a GameObject, and to find a Collection of GameObjects given a Position.
 * It also contains some validity checking methods, and only allows
 * Position/GameObject parings where they are valid.</p>
 * <br><br>
 * <b>Responsibilities</b><br>
 * <p>The ActualMap object is responsible for managing the relationship
 * between GameObjects and Positions, and is also responsible for ensuring
 * that all GameObject/Position parings are legal.</p>
 * <br><br>
 * <b>Collaborators</b><br>
 * <ul>
 * <li>GameObject - Client: GameObjects rely on The actual map to be
 * associated with positions.  Without the Actual Map, They would not
 * have a presence in the game world</li>
 * <li>Position - Subcontractor: the Position class is used by the Actual
 * map to interact with the concept of positions</li>
 * </ul>
 * <br><br>
 * <b>Implementor</b> - Brent <br>
 * <b>Testor</b> - Bruce <br>
 * @author Brent
 *
 */
public class ActualMap implements PathfindingMap
{
	private MapTileConfiguration myConfig;
	private Map<Position, ArrayList<GameObject>> myPositionMapping;
	private Map<GameObject, Position> myGameObjectMapping;
	private MovementValidator myValidator;

	/**
	 * Creates a new Actual Map instance
	 *
	 * @param rows - the number of rows
	 * @param cols - the number of columns
	 * @param config
	 */
	public ActualMap(int rows, int cols, MapTileConfiguration config)
	{
		myConfig = config;
		myValidator = new MovementValidator(this);


		Position.setXBoundary(cols);
		Position.setYBoundary(rows);

		myPositionMapping = new HashMap<Position, ArrayList<GameObject>>();
		myGameObjectMapping = new HashMap<GameObject, Position>();
	}

	/**
	 * Returns all GameObjects at the position p
	 * @param p - p
	 * @return a java.util.ArrayList of GameObjects
	 */
	public ArrayList<GameObject> findObject(Position p)
	{
		return myPositionMapping.get(p);
	}

	/**
	 * Returns the position of the given GameObject
	 *
	 * @param go - GameObject to be found
	 * @return a Position
	 */
	public Position findObject(GameObject go)
	{
		return myGameObjectMapping.get(go);
	}

	/**
	 * Returns the GameObject of the provided type at postion p, if it exits,
	 * and null otherwise.
	 *
	 * @param p - the position being queried
	 * @param t - the GameObjectType you are looking for
	 * @return a GameObject of the requested type, or null if none exits
	 */
	public GameObject findObject(Position p, GameObjectType t)
	{
		if(findObject(p) != null)
		for(GameObject go : this.findObject(p))
		{
			if(go.getType() == t) return go;
		}

		return null;
	}

	/**
	 * Places a GameObject at the given Position.  returns true if sucessful,
	 * false if the placement is invalid
	 *
	 * @param go - the GameObject to be placed
	 * @param p - the Position in which to place it
	 * @return true if and only if the placement was sucessful
	 */
	public boolean placeObject(GameObject go, Position p)
	{
		if (myValidator.validate(go, p))
		{
			if(!myPositionMapping.containsKey(p))
				myPositionMapping.put(p, new ArrayList<GameObject>());

			myPositionMapping.get(p).add(go);

			myGameObjectMapping.put(go, p);

			return true;
		}
		else return false;
	}

	/**
	 * Moves an object that is already at a map position to a new position.
	 * This is equivalent to calling removeObject, followed by placeObject.
	 * returns true if the movement is sucesful, false if the movement fails
	 * (the unit remains where it was).
	 *
	 * @param go - the GameObject being moved
	 * @param p - the Position to move it to
	 * @return - true if the movement is sucessful
	 */
	public boolean moveObject(GameObject go, Position p)
	{
		if(myValidator.validate(go, p))
		{
			this.removeObject(go);
			this.placeObject(go, p);
			return true;
		} else return false;
	}

	/**
	 * Removes a game object from the map
	 *
	 * @param go - the GameObject to be removed
	 * @return - true if the removal is sucessful
	 */
	public boolean removeObject(GameObject go)
	{
		Position p = myGameObjectMapping.remove(go);
		myPositionMapping.get(p).remove(go);

		if (myPositionMapping.get(p).size() == 0)
			myPositionMapping.remove(p);

		return true;
	}

	/**
	 * Removes all gameobjects from a given position.
	 *
	 * @param p - the position to be cleared
	 * @return - true if the removal is sucessful
	 */
	public boolean removeObjects(Position p)
	{
		boolean removal = true;

		for(GameObject current : this.findObject(p))
			if(!this.removeObject(current)) removal = false;

		return removal;
	}

	/**
	 * Remove the game object with the given type from the mapping.
	 * Returns ture if it is sucessful, false if not (usually because
	 * there is not a GameObject of that type at that position).
	 *
	 * @param p - Position of object to be removed
	 * @param t - type of object to be removed
	 * @return - true if the obejct is removed sucessfully
	 */
	public boolean removeObjects(Position p, GameObjectType t)
	{
		GameObject go = this.findObject(p, t);
		if(go == null) return false;
		else return this.removeObject(go);
	}

	/**
	 * Finds the shortest distance between the two given postions, keeping
	 * in mind the movement rules (as decreed by tile shape).
	 *
	 * @param start - the starting postion
	 * @param end - the ending position
	 * @return - the shortest number of movements neede to move from one positon
	 * to the other
	 */
	public int distanceBetween(Position start, Position end)
	{
		if (start.equals(end)) return 0;
		else
		{
			Position begin = new Position(start.X(), start.Y());
			return 1 + distanceBetween(advancePosition(begin, end), end);
		}
	}

	/**
	 * Returns true if the two positons are adjacent, given the movement rules
	 * based on the tile shape.
	 *
	 * @param a - the first postion
	 * @param b - the second position
	 * @return - true if the distance from a to b is one
	 */
	public boolean adjacent(Position a, Position b)
	{
		return distanceBetween(a, b) == 1;
	}

	/**
	 * Returns a collection of all the positons that are adjacent to the given
	 * position, keeping in mind the movement rules as determined by the tile shape.
	 *
	 * @param a - the positon
	 * @return - a java.util.ArrayList of Positions
	 */
	public ArrayList<Position> adjacentPositions(Position a)
	{
		ArrayList<Position> positions = new ArrayList<Position>();
		Position pos;

		switch (myConfig)
		{
		case Octagonal:

			pos = new Position(a.X() - 1, a.Y() - 1);
			if (pos.isValid()) positions.add(pos);

			pos = new Position(a.X() + 1, a.Y() + 1);
			if (pos.isValid()) positions.add(pos);

		case Hexagonal:

			pos = new Position(a.X(), a.Y() - 1);
			if (pos.isValid()) positions.add(pos);

			pos = new Position(a.X() + 1, a.Y() - 1);
			if (pos.isValid()) positions.add(pos);

			pos = new Position(a.X() + 1, a.Y());
			if (pos.isValid()) positions.add(pos);

			pos = new Position(a.X(), a.Y()+ 1);
			if (pos.isValid()) positions.add(pos);

			pos = new Position(a.X() - 1, a.Y() + 1);
			if (pos.isValid()) positions.add(pos);

			pos = new Position(a.X() - 1, a.Y());
			if (pos.isValid()) positions.add(pos);

			break;
		}

		return positions;
	}
	
	/**
	 * Returns all the positions within a given number of moves from this
	 * position.
	 * 
	 * @param p - the starting position
	 * @param range - the number of positions outward to move
	 * @return a java.util.Collection of all the positions within the range
	 */
	public Collection<Position> positionsInRange(Position p, int range)
	{		
		return findPositionsInRange(range, adjacentPositions(p));
	}

	private Position advancePosition(Position start, Position end)
	{
		// we will go through all the possible cases
		if(start.X() > end.X() && start.Y() > end.Y())
		{
			// end tile is up and left
			if(myConfig == MapTileConfiguration.Octagonal)
			{
				// if we are octagonal, go right for it
				start.setX(start.X() - 1);
				start.setY(start.Y() - 1);
			} else if (myConfig == MapTileConfiguration.Hexagonal)
			{
				// if we are hexagonal, we can't get there - move up
				start.setY(start.Y() - 1);
			}
		} else if (start.X() == end.X() && start.Y() > end.Y())
		{
			if(myConfig == MapTileConfiguration.Octagonal)
			{
				// if we are octagonal, go right for it
				start.setY(start.Y() - 1);
			} else if (myConfig == MapTileConfiguration.Hexagonal)
			{
				// if we are hexagonal, go right for it
				start.setY(start.Y() - 1);
			}
		} else if (start.X() < end.X() && start.Y() > end.Y())
		{
			if(myConfig == MapTileConfiguration.Octagonal)
			{
				// if we are octagonal, go right for it
				start.setX(start.X() + 1);
				start.setY(start.Y() - 1);
			} else if (myConfig == MapTileConfiguration.Hexagonal)
			{
				// if we are hexagonal, go right for it
				start.setX(start.X() + 1);
				start.setY(start.Y() - 1);
			}
		} else if (start.X() < end.X() && start.Y() == end.Y())
		{
			if(myConfig == MapTileConfiguration.Octagonal)
			{
				// if we are octagonal, go right for it
				start.setX(start.X() + 1);
			} else if (myConfig == MapTileConfiguration.Hexagonal)
			{
				// if we are hexagonal, go right for it
				start.setX(start.X() + 1);
			}
		} else if (start.X() < end.X() && start.Y() < end.Y())
		{
			if(myConfig == MapTileConfiguration.Octagonal)
			{
				// if we are octagonal, go right for it
				start.setX(start.X() + 1);
				start.setY(start.Y() + 1);
			} else if (myConfig == MapTileConfiguration.Hexagonal)
			{
				// if we are hexagonal, we just go down
				start.setY(start.Y() + 1);
			}
		} else if (start.X() == end.X() && start.Y() < end.Y())
		{
			if(myConfig == MapTileConfiguration.Octagonal)
			{
				// if we are octagonal, go right for it
				start.setY(start.Y() + 1);
			} else if (myConfig == MapTileConfiguration.Hexagonal)
			{
				// if we are hexagonal, go right for it
				start.setY(start.Y() + 1);
			}
		} else if (start.X() > end.X() && start.Y() < end.Y())
		{
			if(myConfig == MapTileConfiguration.Octagonal)
			{
				// if we are octagonal, go right for it
				start.setX(start.X() - 1);
				start.setY(start.Y() + 1);
			} else if (myConfig == MapTileConfiguration.Hexagonal)
			{
				// if we are hexagonal, go right for it
				start.setX(start.X() - 1);
				start.setY(start.Y() + 1);
			}

		}  else if (start.X() > end.X() && start.Y() == end.Y())
		{
			if(myConfig == MapTileConfiguration.Octagonal)
			{
				// if we are octagonal, go right for it
				start.setX(start.X() - 1);
			} else if (myConfig == MapTileConfiguration.Hexagonal)
			{
				// if we are hexagonal, go right for it
				start.setX(start.X() - 1);
			}
		}

		return start;
	}
	
	private Collection<Position> findPositionsInRange(int range, Collection<Position> positions)
	{
		if(range == 0) return positions;
		else
		{
			Vector<Position> temp = new Vector<Position>(positions);
			
			for(Position p : temp)
			{
				Collection<Position> aPos = this.adjacentPositions(p);
				for(Position current : aPos)
				{
					if(!positions.contains(current)) positions.add(current);
				}
			}
			
			return findPositionsInRange(range - 1, positions);
		}
	}

	/* (non-Javadoc)
	 * @see src.model.gameWorld.pathfinding.PathfindingMap#getDirectionTo(src.model.gameWorld.pathfinding.Position, src.model.gameWorld.pathfinding.Position)
	 */
	public Direction2D getDirectionTo(Position start, Position destination)
	{
		return new Direction2D(new Vector2D(new geometry.Point2D(distanceBetween(start, destination), 0)));
	}

	/* (non-Javadoc)
	 * @see src.model.gameWorld.pathfinding.PathfindingMap#getTerrain(src.model.gameWorld.pathfinding.Position)
	 */
	public GameObject getTerrain(Position loc)
	{
		return findObject(loc, GameObjectType.TERRAIN);
	}
}